──────────────────────────────────── ~S44PLAY.Xのココがポイント~ 2.FMPフォーマット 鎌田 誠 ──────────────────────────────────── ━─────────────────────────────────── FMPフォーマットとは ──────────────────────────────────── FMP フォーマットというのは、X68k の FM 音源 LSI(YM2151)の TL(Total Level)レジスタに一定の間隔で直接流し込める状態になっている音声データの フォーマットです。一定の間隔で音の波の変位を直接表しているデータの羅列で あることに変わりはないので、PCM の一種と言えます。FM-PCM を縮めて FMP と 呼ぶことにしました。これは私が S44PLAY.X のために勝手に付けた呼び方です。 X68k の FM 音源 LSI(YM2151)で、データのコンバートを行わずに直接再生 することができる PCM データのフォーマットは、FMP フォーマットだけです。 実際には FMP フォーマットの PCM データを再生する場合にも各サンプリングデ ータをどのチャンネルに流し込むかを再生時にプログラムでリアルタイムに決定 しなければならないので、“直接再生することができる”という表現にはやや語 弊がありますが、FMP フォーマットの PCM データが他のフォーマットの PCM デ ータよりも圧倒的に FM 音源で再生しやすいことは確かです。 一般的な PCM データ(拡張子が .S44 のファイルなど)を FM 音源で再生す る場合はサンプリング周波数や PCM→FMP の変換を行わなければならないので再 生の負荷が大きくなりますが、PCM データをあらかじめ特定のサンプリング周波 数の FMP フォーマットに変換しておけば、データの変換を伴わずに再生できる ので、遅い機種でも再生できるようになる場合があります。 ━─────────────────────────────────── FMP フォーマットの概要 ──────────────────────────────────── FMP フォーマットのデータは、FM 音源 LSI(YM2151)の TL レジスタに直接 叩き込む数値を時間の順序で並べたものです。FMP フォーマットでは、1 つのチ ャンネルの 1 つのサンプリングデータを 8 ビットで表現します。値の意味は次 の通りです。 FMP フォーマットのデータ $01~$7F 音の変位を示します。$01 が最大、$7F が最小(無音)。 $FF 符号が反転することを示します。 なお、FMP フォーマットのデータに $00 がないのは、$00 は再生エンジンが 内部で制御コードとして使用するためです。 FMP フォーマットのデータは、FM 音源の TL レジスタ(FM 音源 LSI のアド レス $60~$7F)に直接書き込むことができるようになっています。 TL レジスタの配置 FM 音源アドレス 7 6 5 4 3 2 1 0 ┌─┬─┬─┬─┬─┬─┬─┬─┐ $60~$7F│×│ TL │ └─┴─┴─┴─┴─┴─┴─┴─┘ TL レジスタのビット 7 が未使用になっているところに注目して下さい。ここ には何を書き込んでも無視されます。FMP フォーマットで $FF を符号の反転コ ードにした理由は、下位 7 ビットを $7F にすることで使用中のチャンネルを無 音にすると同時にビット 7 をセットしておくことで再生エンジンに符号の反転 を知らせて FM 音源のチャンネルを変更させるためです。現在の S44PLAY.X は 音質を上げるために変位が + 側の音と - 側の音で使用する FM 音源のチャンネ ルを変えてあるのです。 実際に出力される音の変位はほぼ 10^(-0.75/20*TL) に比例する値になります。 TL レジスタの設定値に対して音の変位が指数関数的に変化します。 ステレオの場合は、left0→right0→left1→right1→… の順序でデータを編 み込んでゆきます。ただ、ここで leftN と rightN を再生するタイミングがサ ンプリング間隔の半分だけずれていることに注意して下さい。拡張子が .S44 の ファイルでは leftN と rightN が同時に再生されますが、FMP フォーマットの leftN と rightN は同時に再生されません。 ━─────────────────────────────────── PCM→FMP変換 ──────────────────────────────────── まず、絶対値の変換を考えます。前述のように、TL レジスタの設定値に対し て音の変位は指数関数的に変化します。従って、線形な変位を表した PCM デー タを TL レジスタに設定する値に変換するには対数計算を行わなければなりませ ん。当然、いちいち対数計算を行っていては遅くなってしまうので、テーブルを 使います。16 ビットの PCM データを変換するために 64KB のテーブルを用意し ます。この 64KB のテーブルを作るときにも対数計算は行いません。S44PLAY.X は内部に次のようなテーブルを持っていて、これを使って 64KB の PCM→FMP 変 換テーブルを作ります。 対数テーブルを作るためのテーブル(preconv.s より抜粋) ┌──────────────────────────────── │;(L(TL)-L(127))*32768のテーブル │; L(TL):=10^(-0.75/20*TL) │ .align 4,$2048 │half_tl_table: │ .dc.s 0.0249790331564598439725118169021/VOL ;TL=126.5 │ .dc.s 0.0782918709331476020763667840649/VOL ;TL=125.5 │ .dc.s 0.136412699922336602786363931527/VOL ;TL=124.5 │ .dc.s 0.199775126370918425052560396896/VOL ;TL=123.5 │ .dc.s 0.268851861085055585204731577729/VOL ;TL=122.5 │ .dc.s 0.344158246055009330577173283454/VOL ;TL=121.5 │ .dc.s 0.426256099126837360489754730109/VOL ;TL=120.5 │ .dc.s 0.515757905403856993662565002971/VOL ;TL=119.5 │ .dc.s 0.613331386647521673665138088852/VOL ;TL=118.5 │ .dc.s 0.719704482767396026166953362516/VOL ;TL=117.5 │ .dc.s 0.835670782564275638328249586056/VOL ;TL=116.5 │ .dc.s 0.962095444242118369519663701653/VOL ;TL=115.5 │ .dc.s 1.09992164985833884739083589277/VOL ;TL=114.5 │ .dc.s 1.25017764186542639810460404596/VOL ;TL=113.5 │ .dc.s 1.41398439423949693643416771827/VOL ;TL=112.5 │ .dc.s 1.59256397542567932912798377748/VOL ;TL=111.5 │ .dc.s 1.78724866549148627199992039775/VOL ;TL=110.5 │ .dc.s 1.9994908955060339169251943125/VOL ;TL=109.5 │ .dc.s 2.2308740832971310441830848046/VOL ;TL=108.5 │ .dc.s 2.48312444642562093035569302004/VOL ;TL=107.5 │ .dc.s 2.7581238805068177961204790857/VOL ;TL=106.5 │ .dc.s 3.05792399895682477259486919328/VOL ;TL=105.5 │ .dc.s 3.38476143890624676944056994051/VOL ;TL=104.5 │ .dc.s 3.74107454746996202854224690479/VOL ;TL=103.5 │ .dc.s 4.12952157285966282655455833564/VOL ;TL=102.5 │ .dc.s 4.55300049605264658143806178966/VOL ;TL=101.5 │ .dc.s 5.01467065096959003491609135695/VOL ;TL=100.5 │ .dc.s 5.51797629445708127579538962277/VOL ;TL=99.5 │ .dc.s 6.06667230191706194013552490339/VOL ;TL=98.5 │ .dc.s 6.66485218028356717258122173868/VOL ;TL=97.5 │ .dc.s 7.31697860733555307749532837661/VOL ;TL=96.5 │ .dc.s 8.02791672518214930438951651899/VOL ;TL=95.5 │ .dc.s 8.80297043630397886950532805413/VOL ;TL=94.5 │ .dc.s 9.64792197293453999512298113455/VOL ;TL=93.5 │ .dc.s 10.5690750349861618635689602561/VOL ;TL=92.5 │ .dc.s 11.5733018183479152381211218609/VOL ;TL=91.5 │ .dc.s 12.6680942844066978898191874225/VOL ;TL=90.5 │ .dc.s 13.8616200532840539420906962171/VOL ;TL=89.5 │ .dc.s 15.1627833377761835296985297871/VOL ;TL=88.5 │ .dc.s 16.5812913725904012546005717612/VOL ;TL=87.5 │ .dc.s 18.1277268344685641792656189175/VOL ;TL=86.5 │ .dc.s 19.8136267934825696418567047588/VOL ;TL=85.5 │ .dc.s 21.6515687845123606900904016501/VOL ;TL=84.5 │ .dc.s 23.6552646410364842728276505249/VOL ;TL=83.5 │ .dc.s 25.8396627912754194945564937378/VOL ;TL=82.5 │ .dc.s 28.2210597798606648847674248799/VOL ;TL=81.5 │ .dc.s 30.8172218470289422448988826375/VOL ;TL=80.5 │ .dc.s 33.6475174723743143620728005205/VOL ;TL=79.5 │ .dc.s 36.7330618719913062790308049138/VOL ;TL=78.5 │ .dc.s 40.0968745270195300569385457903/VOL ;TL=77.5 │ .dc.s 43.7640509188201426903516795659/VOL ;TL=76.5 │ .dc.s 47.7619497520020175495666327316/VOL ;TL=75.5 │ .dc.s 52.1203970620614939019679680501/VOL ;TL=74.5 │ .dc.s 56.8719087303660091704604737974/VOL ;TL=73.5 │ .dc.s 62.0519330665385781521145446469/VOL ;TL=72.5 │ .dc.s 67.6991152680114780690122777743/VOL ;TL=71.5 │ .dc.s 73.8555857297305391028174139176/VOL ;TL=70.5 │ .dc.s 80.567274354923766077463525079/VOL ;TL=69.5 │ .dc.s 87.884253211827079467601961444/VOL ;TL=68.5 │ .dc.s 95.8611100927329297097983146142/VOL ;TL=67.5 │ .dc.s 104.557355762272086666604989389/VOL ;TL=66.5 │ .dc.s 114.037867933174996988874096728/VOL ;TL=65.5 │ .dc.s 124.373375281761810792360717713/VOL ;TL=64.5 │ .dc.s 135.640985114123682928968241141/VOL ;TL=63.5 │ .dc.s 147.924758619610783711874040175/VOL ;TL=62.5 │ .dc.s 161.316338003264118662012682439/VOL ;TL=61.5 │ .dc.s 175.91563017586737016906831082/VOL ;TL=60.5 │ .dc.s 191.831552102239013254268117846/VOL ;TL=59.5 │ .dc.s 209.182843368381805915081516772/VOL ;TL=58.5 │ .dc.s 228.098952029588185086788174853/VOL ;TL=57.5 │ .dc.s 248.721000348307378065894196923/VOL ;TL=56.5 │ .dc.s 271.202837626591836473127625685/VOL ;TL=55.5 │ .dc.s 295.712187987703418455479657691/VOL ;TL=54.5 │ .dc.s 322.431901669821093416624163188/VOL ;TL=53.5 │ .dc.s 351.561319167036500802735824364/VOL ;TL=52.5 │ .dc.s 383.317758394712735100073451992/VOL ;TL=51.5 │ .dc.s 417.938135974096105603800981463/VOL ;TL=50.5 │ .dc.s 455.680734731657619560311176809/VOL ;TL=49.5 │ .dc.s 496.827130599465368014126536802/VOL ;TL=48.5 │ .dc.s 541.684293292088871985834879115/VOL ;TL=47.5 │ .dc.s 590.586876431983712271424749932/VOL ;TL=46.5 │ .dc.s 643.899714208671470375279717095/VOL ;TL=45.5 │ .dc.s 702.020543197860471085276864557/VOL ;TL=44.5 │ .dc.s 765.382969646442293351473329926/VOL ;TL=43.5 │ .dc.s 834.459704360579453503644510759/VOL ;TL=42.5 │ .dc.s 909.766089330533198876086216484/VOL ;TL=41.5 │ .dc.s 991.863942402361228788667663138/VOL ;TL=40.5 │ .dc.s 1081.365748679380861961477936/VOL ;TL=39.5 │ .dc.s 1178.93922992304554196405102188/VOL ;TL=38.5 │ .dc.s 1285.31232604291989446586629555/VOL ;TL=37.5 │ .dc.s 1401.27862583979950662716251909/VOL ;TL=36.5 │ .dc.s 1527.70328751764223781857663468/VOL ;TL=35.5 │ .dc.s 1665.5294931338627156897488258/VOL ;TL=34.5 │ .dc.s 1815.78548514095026640351697899/VOL ;TL=33.5 │ .dc.s 1979.5922375150208047330806513/VOL ;TL=32.5 │ .dc.s 2158.17181870120319742689671051/VOL ;TL=31.5 │ .dc.s 2352.85650876701014029883333078/VOL ;TL=30.5 │ .dc.s 2565.09873878155778522410724553/VOL ;TL=29.5 │ .dc.s 2796.48192657265491248199773763/VOL ;TL=28.5 │ .dc.s 3048.73228970114479865460595307/VOL ;TL=27.5 │ .dc.s 3323.73172378234166441939201873/VOL ;TL=26.5 │ .dc.s 3623.53184223234864089378212631/VOL ;TL=25.5 │ .dc.s 3950.36928218177063773948287354/VOL ;TL=24.5 │ .dc.s 4306.68239074548589684115983782/VOL ;TL=23.5 │ .dc.s 4695.12941613518669485347126867/VOL ;TL=22.5 │ .dc.s 5118.60833932817044973697472269/VOL ;TL=21.5 │ .dc.s 5580.27849424511390321500428998/VOL ;TL=20.5 │ .dc.s 6083.5841377326051440943025558/VOL ;TL=19.5 │ .dc.s 6632.28014519258580843443783642/VOL ;TL=18.5 │ .dc.s 7230.46002355909104088013467171/VOL ;TL=17.5 │ .dc.s 7882.58645061107694579424130964/VOL ;TL=16.5 │ .dc.s 8593.52456845767317268842945202/VOL ;TL=15.5 │ .dc.s 9368.57827957950273780424098716/VOL ;TL=14.5 │ .dc.s 10213.5298162100638634218940676/VOL ;TL=13.5 │ .dc.s 11134.6828782616857318678731891/VOL ;TL=12.5 │ .dc.s 12138.9096616234391064200347939/VOL ;TL=11.5 │ .dc.s 13233.7021276822217581181003555/VOL ;TL=10.5 │ .dc.s 14427.2278965595778103896091502/VOL ;TL=9.5 │ .dc.s 15728.3911810517073979974427202/VOL ;TL=8.5 │ .dc.s 17146.8992158659251228994846942/VOL ;TL=7.5 │ .dc.s 18693.3346777440880475645318505/VOL ;TL=6.5 │ .dc.s 20379.2346367580935101556176918/VOL ;TL=5.5 │ .dc.s 22217.1766277878845583893145831/VOL ;TL=4.5 │ .dc.s 24220.8724843120081411265634579/VOL ;TL=3.5 │ .dc.s 26405.2706345509433628554066708/VOL ;TL=2.5 │ .dc.s 28786.6676231361887530663378129/VOL ;TL=1.5 │ ;0は使わない │* .dc.s 31382.8296903044661131977955705/VOL ;TL=0.5 │ .dc.s 9999999999.0 ;番兵 │ なお、再生ボリュームの調整は 64KB の PCM→FMP 変換テーブルを生成する段 階で行われるので、ボリュームの指定の有無は再生処理そのものの負荷を変化さ せません。 さて、現在の S44PLAY.X は + 側の変位と - 側の変位を異なるチャンネルで 再生することで音質を上げています。対数変換のテーブルを引くだけでは変位の 絶対値が変換されるだけで、符号の変化を示すビット 7 が生成できていません。 FMP フォーマットのデータのビット 7 を生成するために、PCM→FMP 変換ルーチ ンが PCM データの実際の符号の変化を監視して、符号が変わったところで FMP フォーマットのデータを $FF にするという処理を行います。このとき、符号が 変わる前後で絶対値が小さいほうを $FF にすることでノイズを減らします。 では、最も簡単な、周波数変換を伴わない場合の PCM→FMP 変換ルーチン(マ クロ)を見て下さい。Atbl に PCM→FMP 変換テーブル(64KB)のベース位置 (先頭+32KB の位置)、Asrc に入力データのアドレス、Adst に出力バッファの アドレス、Alim に入力データの末尾のアドレス+1 が設定された状態で、例えば PRECONV_EQUAL_LQ 0 PRECONV_EXIT_EQUAL_LQ 0 とすると、PCM データからモノラルの FMP フォーマットのデータを生成するル ーチンになります(ステレオのときは Left と Right を分けて処理します)。 最も基本的な PCM→FMP 変換(preconv.mac,preconv.s より抜粋) ┌──────────────────────────────── │;<Low Quality> │;間隔が同じ→周波数が同じ │; side 0=Mono,1=StereoLeft,2=StereoRight │;Ddat dn │;Rpre dn/an │;Dtmp dn │;Atbl an │;Asrc an │;Adst an │;Ajmp an │;Alim dn/an │PRECONV_EQUAL_LQ .macro side │ move.w preconv_pre_&side,Rpre │ move.l preconv_jmp_&side,Ajmp │ move.l Ajmp,Dtmp │ beq @spp │ jmp (Ajmp) │ │@npp: move.b #$7F,(Adst)+ │ if side,<addq.l #1,Adst> │ bra @vpp │@dpp: move.b (Atbl,Rpre.w),(Adst)+ │ if side,<addq.l #1,Adst> │@vpp: move.w Ddat,Rpre │ cmp.l Alim,Asrc │ bhs @epp │@spp: GET_DATA_&side Asrc,Ddat,Dtmp │ bgt @dpp │ beq @dpm │ move.w Ddat,Dtmp │ add.w Rpre,Dtmp │ blt @xmm │@dpm: move.b (Atbl,Rpre.w),(Adst)+ │ if side,<addq.l #1,Adst> │@vpm: move.w Ddat,Rpre │ cmp.l Alim,Asrc │ bhs @epm │@spm: GET_DATA_&side Asrc,Ddat,Dtmp │ bgt @npp │ beq @npz │@xmm: st.b (Adst)+ │ if side,<addq.l #1,Adst> │ bra @vmm │@npz: move.b #$7F,(Adst)+ │ if side,<addq.l #1,Adst> │ bra @vpm │ │@nmm: move.b #$7F,(Adst)+ │ if side,<addq.l #1,Adst> │ bra @vmm │@dmm: move.b (Atbl,Rpre.w),(Adst)+ │ if side,<addq.l #1,Adst> │@vmm: move.w Ddat,Rpre │ cmp.l Alim,Asrc │ bhs @emm │@smm: GET_DATA_&side Asrc,Ddat,Dtmp │ blt @dmm │ beq @dmp │ move.w Ddat,Dtmp │ add.w Rpre,Dtmp │ bgt @xpp │@dmp: move.b (Atbl,Rpre.w),(Adst)+ │ if side,<addq.l #1,Adst> │@vmp: move.w Ddat,Rpre │ cmp.l Alim,Asrc │ bhs @emp │@smp: GET_DATA_&side Asrc,Ddat,Dtmp │ blt @nmm │ beq @nmz │@xpp: st.b (Adst)+ │ if side,<addq.l #1,Adst> │ bra @vpp │@nmz: move.b #$7F,(Adst)+ │ if side,<addq.l #1,Adst> │ bra @vmp │ │@epp: lea.l (@spp,pc),Ajmp │ bra @done │ │@epm: lea.l (@spm,pc),Ajmp │ bra @done │ │@emm: lea.l (@smm,pc),Ajmp │ bra @done │ │@emp: lea.l (@smp,pc),Ajmp │ bra @done │ │@done: │ .endm │PRECONV_EXIT_EQUAL_LQ .macro side │ move.l Ajmp,preconv_jmp_&side │ move.w Rpre,preconv_pre_&side │ .endm かなり読みにくいマクロになっていますが、これはソースをできるだけ短くす るために入力データ(PCM データ)のフォーマットの違いをマクロのレベルで吸 収するようになっているからです。例えば GET_DATA_0 というマクロはモノラル の PCM データを 1 個レジスタに取り出してくるだけのマクロですが、エンディ アンや入力データのチャンネル数の違いをここで吸収します。 マクロ GET_DATA_0 の定義の例(preconv.s より抜粋) ┌──────────────────────────────── │;ステレオ→モノラル,16bit,big-endian │GET_DATA_0 .macro src,dat,tmp │ move.w (src)+,dat │ add.w (src)+,dat │ bvc @skip │@over: │ roxr.w #1,dat ;常にvc │ bra @done │@skip: │ asr.w #1,dat ;常にvc │@done: │ .endm ┌──────────────────────────────── │;モノラル→モノラル,16bit,big-endian │GET_DATA_0 .macro src,dat,tmp │ move.w (src)+,dat ;常にvc │ .endm ┌──────────────────────────────── │;ステレオ→モノラル,16bit,little-endian │GET_DATA_0 .macro src,dat,tmp │ move.w (src)+,dat │ move.w (src)+,tmp │ rol.w #8,dat │ rol.w #8,tmp │ add.w tmp,dat │ bvc @skip │@over: │ roxr.w #1,dat ;常にvc │ bra @done │@skip: │ asr.w #1,dat ;常にvc │@done: │ .endm ┌──────────────────────────────── │;モノラル→モノラル,16bit,little-endian │GET_DATA_0 .macro src,dat,tmp │ move.w (src)+,dat │ rol.w #8,dat ;常にvc │ .endm │ マクロの全体は抜粋できるような規模ではないので、全体を把握したい人はソ ースリスト(特に preconv.mac と preconv.s)をじっくり読んでみて下さい。 ━─────────────────────────────────── PCM→FMP 変換は S44PLAY.X の基本的なところなのでちゃんと解説したかった のですが、(最適化のせいもあって)マクロの塊が複雑すぎて解説になっていま せんね。そもそもアセンブラの文法を知らなければチンプンカンプンでしょうし。 せめて S44PLAY.X のソースを覗くきっかけにしていただけるとよいのですが…。 (EOF)